using System;
using System.Collections.Generic;

namespace CHIP8Emulator {
    internal abstract class OpcodeProcessor {
        /* Base class to execute all groups of opcodes */
        protected readonly CHIP8Device _device; // CHIP8 machine
        private const int OPCODE_TYPE_POS = 12;
        private readonly OpcodeType _opcodeType;

        protected OpcodeProcessor(CHIP8Device device, OpcodeType opcodeType) {
            _device = device;
            _opcodeType = opcodeType;
        }

        public void Process(ushort opcode) {
            if ((OpcodeType) (opcode >> OPCODE_TYPE_POS) == _opcodeType) // Check if opcode matches processor
                InnerProcess(opcode); // Execute opcode
            else
                throw new Exception(string.Format("Opcode 0x{0} cannot be processed by {1} (Type {2})", opcode.ToString("X" + CHIP8Device.OPCODE_SIZE * 2), GetType().Name, _opcodeType));
        }

        protected abstract void InnerProcess(ushort opcode);

        /* Increment program counter */
        protected void IncrementPC() { 
            _device.PC += CHIP8Device.OPCODE_SIZE;
        }


        /* Get last 8 bits from opcode */
        protected static byte GetNN(ushort opcode) {
            return (byte) (opcode & 0x00FF);
        }

        /* Get last 12 bits from opcode */
        protected static ushort GetNNN(ushort opcode) {
            return (ushort) (opcode & 0x0FFF);
        }

        /* Get 0x_X__ */
        protected static int GetX(ushort opcode) {
            return (opcode >> 8) & 0x0F;
        }

        /* Get 0x__Y_ */
        protected static int GetY(ushort opcode) {
            return (opcode >> 4) & 0x0F;
        }

        /* Get 0x___Z */
        protected static int GetZ(ushort opcode) {
            return opcode & 0x0F;
        }

        protected static void ThrowInvaliOpcodeException(ushort opcode) {
            throw new Exception(string.Format("Invalid Opcode 0x{0}.", opcode.ToString("X" + CHIP8Device.OPCODE_SIZE * 2)));
        }
    }

    internal class OpcodeProcessor_8XY_ : OpcodeProcessor {
        // 8XY0 	Sets VX to the value of VY.
        // 8XY1 	Sets VX to VX or VY.
        // 8XY2 	Sets VX to VX and VY.
        // 8XY3 	Sets VX to VX xor VY.
        // 8XY4 	Adds VY to VX. VF is set to 1 when there's a carry, and to 0 when there isn't.
        // 8XY5 	VY is subtracted from VX. VF is set to 0 when there's a borrow, and 1 when there isn't.
        // 8XY6 	Shifts VX right by one. VF is set to the value of the least significant bit of VX before the shift. [2]
        // 8XY7 	Sets VX to VY minus VX. VF is set to 0 when there's a borrow, and 1 when there isn't.
        // 8XYE 	Shifts VX left by one. VF is set to the value of the most significant bit of VX before the shift. [2]
        private readonly Action<ushort>[] _op = new Action<ushort>[(int)((Op8XY)Enum.GetValues(typeof(Op8XY)).GetValue(Enum.GetValues(typeof(Op8XY)).Length - 1)) + 1];

        private enum Op8XY {
            _0x0 = 0x0,
            _0x1 = 0x1,
            _0x2 = 0x2,
            _0x3 = 0x3,
            _0x4 = 0x4,
            _0x5 = 0x5,
            _0x6 = 0x6,
            _0x7 = 0x7,
            _0xE = 0xE,
        }

        public OpcodeProcessor_8XY_(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {
            _op[(int) Op8XY._0x0] = opcode => _device.V[GetX(opcode)] = _device.V[GetY(opcode)];
            _op[(int) Op8XY._0x1] = opcode => _device.V[GetX(opcode)] |= _device.V[GetY(opcode)];
            _op[(int) Op8XY._0x2] = opcode => _device.V[GetX(opcode)] &= _device.V[GetY(opcode)];
            _op[(int) Op8XY._0x3] = opcode => _device.V[GetX(opcode)] ^= _device.V[GetY(opcode)];
            _op[(int) Op8XY._0x4] = opcode => {
                                        ushort vx = _device.V[GetX(opcode)];
                                        vx += _device.V[GetY(opcode)];
                                        _device.V[GetX(opcode)] = (byte) (vx & 0x00FF);
                                        _device.V[0xF] = Convert.ToByte(vx > 0xFF);
                                    };
            _op[(int) Op8XY._0x5] = opcode => {
                                        short vx = _device.V[GetX(opcode)];
                                        vx -= _device.V[GetY(opcode)];
                                        _device.V[GetX(opcode)] = (byte) (vx & 0x00FF);
                                        _device.V[0xF] = Convert.ToByte(vx >= 0x00);
                                    };
            _op[(int) Op8XY._0x6] = opcode => {
                                        _device.V[0xF] = (byte) (_device.V[GetX(opcode)] & 1);
                                        _device.V[GetX(opcode)] >>= 1;
                                    };
            _op[(int) Op8XY._0x7] = opcode => {
                                        short vx = _device.V[GetY(opcode)];
                                        vx -= _device.V[GetX(opcode)];
                                        _device.V[GetX(opcode)] = (byte) (vx & 0x00FF);
                                        _device.V[0xF] = Convert.ToByte(vx >= 0x00);
                                    };
            _op[(int) Op8XY._0xE] = opcode => {
                                        _device.V[0xF] = (byte) (_device.V[GetX(opcode)] & 0x80);
                                        _device.V[GetX(opcode)] <<= 1;
                                    };
        }

        protected override void InnerProcess(ushort opcode) {
            _op[GetZ(opcode)](opcode);
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_7XNN : OpcodeProcessor {
        // 7XNN 	Adds NN to VX.
        public OpcodeProcessor_7XNN(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            _device.V[GetX(opcode)] += GetNN(opcode);
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_6XNN : OpcodeProcessor {
        // 6XNN 	Sets VX to NN.
        public OpcodeProcessor_6XNN(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            _device.V[GetX(opcode)] = GetNN(opcode);
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_CXNN : OpcodeProcessor {
        // CXNN 	Sets VX to a random number and NN.
        private readonly Random _random = new Random(DateTime.Now.Millisecond);
        public OpcodeProcessor_CXNN(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            _device.V[GetX(opcode)] = (byte) (_random.Next() & GetNN(opcode));
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_DXYN : OpcodeProcessor {
        // DXYN 	Draws a sprite at coordinate (VX, VY) that has a width of 8 pixels and a height of N pixels. 
        //          Each row of 8 pixels is read as bit-coded starting from memory location I; 
        //          I value doesn't change after the execution of this instruction. 
        //          As described above, VF is set to 1 if any screen pixels are flipped from set to unset when the sprite is drawn, 
        //          and to 0 if that doesn't happen.
        public OpcodeProcessor_DXYN(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            _device.V[0xF] = 0;
            int x0 = _device.V[GetX(opcode)];
            int y0 = _device.V[GetY(opcode)];
            for (int y = 0; y < GetZ(opcode); y++) {
                byte row = _device.M[_device.I + y];
                _device.V[0xF] |= (byte)(_device.SetSpriteRow(x0, y0 + y, row) ? 1 : 0);
            }
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_FX__ : OpcodeProcessor {
        // FX07 	Sets VX to the value of the delay timer.
        // FX0A 	A key press is awaited, and then stored in VX.
        // FX15 	Sets the delay timer to VX.
        // FX18 	Sets the sound timer to VX.
        // FX1E 	Adds VX to I.
        // FX29 	Sets I to the location of the sprite for the character in VX. Characters 0-F (in hexadecimal) are represented by a 4x5 font.
        // FX33 	Stores the Binary-coded decimal representation of VX at the addresses I, I plus 1, and I plus 2.
        // FX55 	Stores V0 to VX in memory starting at address I. [3]
        // FX65 	Fills V0 to VX with values from memory starting at address I. [3]
        private enum OpFX :byte{
            FX07 = 0x07,
            FX0A = 0x0A,
            FX15 = 0x15,
            FX18 = 0x18,
            FX1E = 0x1E,
            FX29 = 0x29,
            FX33 = 0x33,
            FX55 = 0x55,
            FX65 = 0x65,
        }
        private readonly Action<ushort>[] _op = new Action<ushort>[(int) ((OpFX) Enum.GetValues(typeof(OpFX)).GetValue(Enum.GetValues(typeof(OpFX)).Length - 1)) + 1];

        public OpcodeProcessor_FX__(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {
            _op[(int) OpFX.FX07] = opcode => _device.V[GetX(opcode)] = _device.DelayTimer;
            _op[(int) OpFX.FX0A] = opcode => _device.V[GetX(opcode)] = _device.WaitForKey();
            _op[(int) OpFX.FX15] = opcode => _device.DelayTimer = _device.V[GetX(opcode)];
            _op[(int) OpFX.FX18] = opcode => _device.SoundTimer = _device.V[GetX(opcode)];
            _op[(int) OpFX.FX1E] = opcode => _device.I += _device.V[GetX(opcode)];
            _op[(int) OpFX.FX29] = opcode => _device.I = _device.GetSymbolAddress((Symbol) _device.V[GetX(opcode)]);
            _op[(int) OpFX.FX33] = opcode => {
                                             byte vx = _device.V[GetX(opcode)];
                                             _device.M[_device.I + 0] = (byte) ((vx) / 100);
                                             _device.M[_device.I + 1] = (byte) ((vx - _device.M[_device.I + 0]) / 10);
                                             _device.M[_device.I + 2] = (byte) ((vx - _device.M[_device.I + 0] - _device.M[_device.I + 1]) / 1);
                                         };
            _op[(int) OpFX.FX55] = opcode => {
                                           for (int idx = 0; idx <= GetX(opcode); idx++)
                                               _device.M[_device.I + idx] = _device.V[idx];
                                       };
            _op[(int) OpFX.FX65] = opcode => {
                                             for (int idx = 0; idx <= GetX(opcode); idx++)
                                                 _device.V[idx] = _device.M[_device.I + idx];
                                         };
        }

        protected override void InnerProcess(ushort opcode) {
            _op[GetNN(opcode)](opcode);
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_EX9E_EXA1 : OpcodeProcessor {
        // EX9E 	Skips the next instruction if the key stored in VX is pressed.
        // EXA1 	Skips the next instruction if the key stored in VX isn't pressed.
        private const byte BYTE_IS_PRESSED = 0x9E;
        private const byte BYTE_IS_NOT_PRESSED = 0xA1;
        public OpcodeProcessor_EX9E_EXA1(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            byte nn = GetNN(opcode);
            if (nn == BYTE_IS_PRESSED && _device.IsPressed(_device.V[GetX(opcode)]))
                IncrementPC();
            else if (nn == BYTE_IS_NOT_PRESSED && !_device.IsPressed(_device.V[GetX(opcode)]))
                IncrementPC();
            else if (nn != BYTE_IS_PRESSED && nn != BYTE_IS_NOT_PRESSED)
                ThrowInvaliOpcodeException(opcode);
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_5XY0 : OpcodeProcessor_3XNN {
        // 5XY0 	Skips the next instruction if VX equals VY.
        public OpcodeProcessor_5XY0(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            _device.PC += _device.V[GetX(opcode)] == _device.V[GetY(opcode)] ? CHIP8Device.OPCODE_SIZE : USHORT_0;
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_9XY0 : OpcodeProcessor_3XNN {
        // 9XY0 	Skips the next instruction if VX doesn't equal VY.
        public OpcodeProcessor_9XY0(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            _device.PC += _device.V[GetX(opcode)] != _device.V[GetY(opcode)] ? CHIP8Device.OPCODE_SIZE : USHORT_0;
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_2NNN : OpcodeProcessor {
        // 2NNN 	Calls subroutine at NNN.
        public OpcodeProcessor_2NNN(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            if (_device.Stack.Count < CHIP8Device.STACK_SIZE) {
                _device.Stack.Push(_device.PC);
                _device.PC = GetNNN(opcode);
            }
            else
                throw new Exception(string.Format("Stack overflow at PC = {0}", _device.PC));
        }
    }

    internal class OpcodeProcessor_3XNN : OpcodeProcessor {
        // 3XNN 	Skips the next instruction if VX equals NN.
        protected const ushort USHORT_0 = 0;
        public OpcodeProcessor_3XNN(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            _device.PC += _device.V[GetX(opcode)] == GetNN(opcode) ? CHIP8Device.OPCODE_SIZE : USHORT_0;
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_4XNN : OpcodeProcessor_3XNN {
        // 4XNN 	Skips the next instruction if VX doesn't equal NN.
        public OpcodeProcessor_4XNN(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            _device.PC += _device.V[GetX(opcode)] != GetNN(opcode) ? CHIP8Device.OPCODE_SIZE : USHORT_0;
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_1NNN : OpcodeProcessor {
        // 1NNN 	Jumps to address NNN.
        public OpcodeProcessor_1NNN(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            JumpTo(GetNNN(opcode));
        }

        protected void JumpTo(ushort addr) {
            if (addr <= CHIP8Device.RAM_SIZE)
                _device.PC = addr;
            else
                throw new Exception("Memory address out of bounds.");
        }
    }

    internal class OpcodeProcessor_BNNN : OpcodeProcessor_1NNN {
        // BNNN 	Jumps to the address NNN plus V0.
        public OpcodeProcessor_BNNN(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            JumpTo((ushort) (GetNNN(opcode) + _device.V[0]));
        }
    }

    internal class OpcodeProcessor_ANNN : OpcodeProcessor {
        // ANNN 	Sets I to the address NNN.
        public OpcodeProcessor_ANNN(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            _device.I = GetNNN(opcode);
            IncrementPC();
        }
    }

    internal class OpcodeProcessor_0NNN_00E0_00EE : OpcodeProcessor {
        // 0NNN 	Calls RCA 1802 program at address NNN.
        // 00E0 	Clears the screen.
        // 00EE 	Returns from a subroutine.
        private const ushort CLS_OPCODE_00E0 = 0x00E0;
        private const ushort RET_OPCODE_00EE = 0x00EE;

        public OpcodeProcessor_0NNN_00E0_00EE(CHIP8Device device, OpcodeType opcodeType) : base(device, opcodeType) {}

        protected override void InnerProcess(ushort opcode) {
            if (opcode == CLS_OPCODE_00E0)
                ClearVideoMemory();
            else if (opcode == RET_OPCODE_00EE)
                ReturnFromSubroutine();
            else if (opcode >= CHIP8Device.PC_INIT_VALUE)
                CallProgram(GetNNN(opcode));
            else
                ThrowInvaliOpcodeException(opcode);
        }

        private void CallProgram(ushort addr) {
            _device.Reset(addr);
        }

        private void ReturnFromSubroutine() {
            if (_device.Stack.Count != 0)
                _device.PC = _device.Stack.Pop();
            else
                _device.Stop();
            IncrementPC();
        }

        private void ClearVideoMemory() {
            _device.D = new uint[CHIP8Device.VIDEO_MEMORY_SIZE];
            IncrementPC();
        }
    }
}